#!/usr/bin/python3

import json
import os
import codecs
import setproctitle
import sys
from functools import lru_cache
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Keybinder', '3.0')
from gi.repository import \
    Gtk, Gdk, GLib, GdkPixbuf, Gio, Keybinder  # noqa: E402

__VERSION__ = "1.0.9"


class Main(object):
    def __init__(self):
        self.zoomlevel = 2
        self.app = Gtk.Application.new(
            "org.kryogenix.magnus",
            Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.app.connect("command-line", self.handle_commandline)
        self.app.connect("shutdown", self.handle_shutdown)
        self.resize_timeout = None
        self.window_metrics = None
        self.window_metrics_restored = False
        self.decorations_height = 0
        self.decorations_width = 0
        self.last_x = -1
        self.last_y = -1
        self.refresh_interval = 250
        self.started_by_keypress = False
        # 极小死区设置 (1像素)
        self.deadzone_padding = 1

    def handle_shutdown(self, app):
        if self.started_by_keypress:
            settings = Gio.Settings.new("org.gnome.desktop.a11y.applications")
            val = settings.get_boolean("screen-magnifier-enabled")
            if val:
                settings.set_boolean("screen-magnifier-enabled", False)

    def handle_commandline(self, app, cmdline):
        args = cmdline.get_arguments()
        if hasattr(self, "w"):
            if "--about" in args:
                self.show_about_dialog()
            return 0

        if "--help" in args:
            print("Options:")
            print("  --about")
            print("    显示关于对话框")
            print("  --refresh-interval=120")
            print("    设置刷新间隔（毫秒）")

        for arg in args:
            if arg.startswith("--refresh-interval="):
                parts = arg.split("=")
                if len(parts) == 2:
                    try:
                        rival = int(parts[1])
                        print(f"刷新间隔设置为 {rival}ms")
                        self.refresh_interval = rival
                    except ValueError:
                        pass
            if arg == "--started-by-keypress":
                self.started_by_keypress = True

        self.start_everything_first_time()
        if "--about" in args:
            self.show_about_dialog()
        return 0

    def start_everything_first_time(self, on_window_map=None):
        GLib.set_application_name("Magnus 放大镜")

        # 创建窗口
        self.w = Gtk.ApplicationWindow.new(self.app)
        self.w.set_size_request(400, 300)
        self.w.set_title("Magnus 放大镜")
        self.w.connect("destroy", lambda a: self.app.quit())
        self.w.connect("configure-event", self.read_window_size)
        self.w.connect("configure-event", self.window_configure)
        self.w.connect("size-allocate", self.read_window_decorations_size)
        self.pointer = None

        # 获取指针设备
        default_display = Gdk.Display.get_default()
        default_seat = default_display.get_default_seat()
        pointer_device = default_seat.get_pointer()
        if pointer_device:
            self.pointer = pointer_device

        # 创建标题栏
        head = Gtk.HeaderBar()
        head.set_show_close_button(True)
        head.props.title = "Magnus 放大镜"
        self.w.set_titlebar(head)

        # 缩放选择器
        zoom = Gtk.ComboBoxText.new()
        self.zoom = zoom
        self.zoom_ids = []
        for i in [2, 3, 4, 5, 6]:
            zoom_id = str(i)
            self.zoom_ids.append(zoom_id)
            zoom.append(zoom_id, f"{i}×")
        zoom.set_active(0)
        zoom.connect("changed", self.set_zoom)
        head.pack_end(zoom)

        # 图像显示区域
        self.img = Gtk.Image()
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.add(self.img)
        self.w.add(scrolled_window)

        # 绑定快捷键
        Keybinder.init()
        if Keybinder.supported():
            Keybinder.bind("<Alt><Super>plus", self.zoom_in, zoom)
            Keybinder.bind("<Alt><Super>equal", self.zoom_in, zoom)
            Keybinder.bind("<Alt><Super>minus", self.zoom_out, zoom)

        # 显示窗口
        self.w.show_all()

        self.width = 0
        self.height = 0
        self.window_x = 0
        self.window_y = 0
        GLib.timeout_add(250, self.read_window_size)

        # 确保窗口初始位置正确
        GLib.idle_add(self.ensure_window_in_screen)

        # 启动轮询
        GLib.timeout_add(self.refresh_interval, self.poll)

        # 加载配置
        GLib.idle_add(self._load_config)

    def ensure_window_in_screen(self):
        """确保窗口完全在屏幕显示区域内"""
        if not hasattr(self, 'w'):
            return False

        screen = self.w.get_screen()
        display = screen.get_display()
        monitor = display.get_monitor_at_window(self.w.get_window())
        monitor_geom = monitor.get_geometry()

        x, y = self.w.get_position()
        width, height = self.w.get_size()

        new_x = max(monitor_geom.x, min(x, monitor_geom.x + monitor_geom.width - width))
        new_y = max(monitor_geom.y, min(y, monitor_geom.y + monitor_geom.height - height))

        if new_x != x or new_y != y:
            self.w.move(new_x, new_y)
            self.window_x = new_x
            self.window_y = new_y

        return False

    def zoom_out(self, keypress, zoom):
        current_index = zoom.get_active()
        if current_index > 0:
            zoom.set_active(current_index - 1)
            self.set_zoom(zoom)

    def zoom_in(self, keypress, zoom):
        current_index = zoom.get_active()
        size = zoom.get_model().iter_n_children(None)
        if current_index < size - 1:
            zoom.set_active(current_index + 1)
            self.set_zoom(zoom)

    def read_window_decorations_size(self, win, alloc):
        sz = self.w.get_size()
        self.decorations_width = alloc.width - sz.width
        self.decorations_height = alloc.height - sz.height

    def set_zoom(self, zoom):
        self.zoomlevel = int(zoom.get_active_text()[0])
        self.poll(force_refresh=True)
        self.serialise()

    def read_window_size(self, *args):
        loc = self.w.get_size()
        self.width = loc.width
        self.height = loc.height
        return False

    def show_about_dialog(self, *args):
        about_dialog = Gtk.AboutDialog()
        about_dialog.set_artists(["Stuart Langridge"])
        about_dialog.set_authors(["Stuart Langridge"])
        about_dialog.set_version(__VERSION__)
        about_dialog.set_license_type(Gtk.License.MIT_X11)
        about_dialog.set_website("https://www.kryogenix.org/code/magnus")
        about_dialog.run()
        about_dialog.destroy()

    @lru_cache()
    def makesquares(self, overall_width, overall_height, square_size,
                    value_on, value_off):
        on_sq = list(value_on) * square_size
        off_sq = list(value_off) * square_size
        on_row = []
        off_row = []
        while len(on_row) < overall_width * len(value_on):
            on_row += on_sq
            on_row += off_sq
            off_row += off_sq
            off_row += on_sq
        on_row = on_row[:overall_width * len(value_on)]
        off_row = off_row[:overall_width * len(value_on)]

        on_sq_row = on_row * square_size
        off_sq_row = off_row * square_size

        overall = []
        count = 0
        while len(overall) < overall_width * overall_height * len(value_on):
            overall += on_sq_row
            overall += off_sq_row
            count += 2
        overall = overall[:overall_width * overall_height * len(value_on)]
        return overall

    @lru_cache()
    def get_white_pixbuf(self, width, height):
        square_size = 16
        light = (153, 153, 153, 255)
        dark = (102, 102, 102, 255)
        whole = self.makesquares(width, height, square_size, light, dark)
        arr = GLib.Bytes.new(whole)
        return GdkPixbuf.Pixbuf.new_from_bytes(
            arr, GdkPixbuf.Colorspace.RGB, True, 8,
            width, height, width * len(light))

    def poll(self, force_refresh=False):
        # 确保窗口尺寸有效
        if self.width == 0 or self.height == 0:
            self.width, self.height = self.w.get_size()
            if self.width == 0 or self.height == 0:
                return True

        # 获取鼠标位置
        display = Gdk.Display.get_default()
        if not display:
            return True
            
        try:
            seat = display.get_default_seat()
            pointer = seat.get_pointer()
            if pointer:
                pos_info = pointer.get_position()
                if len(pos_info) == 3:
                    _, x, y = pos_info
                elif len(pos_info) == 2:
                    x, y = pos_info
                else:
                    _, x, y, _ = display.get_pointer()
            else:
                _, x, y, _ = display.get_pointer()
        except Exception as e:
            print(f"获取鼠标位置出错: {e}")
            x, y = self.last_x, self.last_y
            if x == -1 or y == -1:
                return True

        # 检查鼠标位置是否变化
        if x == self.last_x and y == self.last_y and not force_refresh:
            return True
            
        self.last_x, self.last_y = x, y

        # 计算窗口精确边界（含装饰）
        window_left = self.window_x
        window_right = self.window_x + self.width + self.decorations_width
        window_top = self.window_y
        window_bottom = self.window_y + self.height + self.decorations_height

        # 精确死区判断逻辑 - 重点修复左上方区域
        in_deadzone = False
        
        # 1. 检查是否在窗口内部
        if window_left < x < window_right and window_top < y < window_bottom:
            in_deadzone = True
        else:
            # 2. 检查是否在左侧边框外极小死区（仅垂直方向与窗口重叠部分）
            if (x >= window_left - self.deadzone_padding and 
                x <= window_left and 
                y > window_top and  # 使用>而非>=，避免误判左上方区域
                y < window_bottom):
                in_deadzone = True
            # 3. 检查是否在右侧边框外极小死区
            elif (x >= window_right and 
                  x <= window_right + self.deadzone_padding and 
                  y > window_top and 
                  y < window_bottom):
                in_deadzone = True
            # 4. 检查是否在上侧边框外极小死区（仅水平方向与窗口重叠部分）
            elif (y >= window_top - self.deadzone_padding and 
                  y <= window_top and 
                  x > window_left and  # 使用>而非>=，避免误判左上方区域
                  x < window_right):
                in_deadzone = True
            # 5. 检查是否在下侧边框外极小死区
            elif (y >= window_bottom and 
                  y <= window_bottom + self.deadzone_padding and 
                  x > window_left and 
                  x < window_right):
                in_deadzone = True

        # 特殊处理：窗口左上方区域（x < window_left 且 y < window_top）永远不进入死区
        if x < window_left and y < window_top:
            in_deadzone = False

        if in_deadzone:
            # 死区内显示灰白方块
            white = self.get_white_pixbuf(self.width, self.height)
            self.img.set_from_pixbuf(white)
        else:
            # 死区外显示放大内容
            root = Gdk.get_default_root_window()
            if not root:
                return True

            # 计算截取区域
            scaled_width = max(1, self.width // self.zoomlevel)
            scaled_height = max(1, self.height // self.zoomlevel)
            scaled_xoff = scaled_width // 2
            scaled_yoff = scaled_height // 2

            # 获取当前显示器信息
            screen_geom = display.get_monitor_at_point(x, y).get_geometry()
            screen_x = screen_geom.x
            screen_y = screen_geom.y
            screen_width = screen_geom.width
            screen_height = screen_geom.height

            # 确保截取区域在显示器范围内
            x1 = max(screen_x, x - scaled_xoff)
            y1 = max(screen_y, y - scaled_yoff)
            x2 = min(screen_x + screen_width, x1 + scaled_width)
            y2 = min(screen_y + screen_height, y1 + scaled_height)

            capture_width = max(1, x2 - x1)
            capture_height = max(1, y2 - y1)

            # 截取并显示
            try:
                screenshot = Gdk.pixbuf_get_from_window(
                    root, x1, y1, capture_width, capture_height)
                
                if screenshot:
                    scaled_pb = screenshot.scale_simple(
                        self.width, self.height,
                        GdkPixbuf.InterpType.BILINEAR)
                    self.img.set_from_pixbuf(scaled_pb)
                else:
                    white = self.get_white_pixbuf(self.width, self.height)
                    self.img.set_from_pixbuf(white)
            except Exception as e:
                print(f"截图错误: {e}")
                white = self.get_white_pixbuf(self.width, self.height)
                self.img.set_from_pixbuf(white)

        return True

    def window_configure(self, window, ev):
        if not self.window_metrics_restored:
            return False
        if self.resize_timeout:
            GLib.source_remove(self.resize_timeout)
        self.resize_timeout = GLib.timeout_add_seconds(
            1, self.save_window_metrics_after_timeout,
            {"x": ev.x, "y": ev.y, "w": ev.width, "h": ev.height})
        self.window_x = ev.x
        self.window_y = ev.y

    def save_window_metrics_after_timeout(self, props):
        GLib.source_remove(self.resize_timeout)
        self.resize_timeout = None
        self.save_window_metrics(props)

    def save_window_metrics(self, props):
        scr = self.w.get_screen()
        display = scr.get_display()
        monitor_geom = display.get_primary_monitor().get_geometry()
        monitor_width = monitor_geom.width
        monitor_height = monitor_geom.height

        self.window_metrics = {
            "ww": props["w"] / monitor_width,
            "wh": props["h"] / monitor_height,
            "wx": props["x"] / monitor_width,
            "wy": props["y"] / monitor_height
        }
        self.serialise()

    def restore_window_metrics(self, metrics):
        scr = self.w.get_screen()
        display = scr.get_display()
        monitor_geom = display.get_primary_monitor().get_geometry()
        monitor_width = monitor_geom.width
        monitor_height = monitor_geom.height

        width = int(monitor_width * metrics["ww"])
        height = int(monitor_height * metrics["wh"])
        x = int(monitor_width * metrics["wx"])
        y = int(monitor_height * metrics["wy"])

        x = max(monitor_geom.x, min(x, monitor_geom.x + monitor_geom.width - width))
        y = max(monitor_geom.y, min(y, monitor_geom.y + monitor_geom.height - height))

        self.w.set_size_request(width, height)
        self.w.move(x, y)

    def get_cache_file(self):
        return os.path.join(GLib.get_user_cache_dir(), "magnus.json")

    def serialise(self, *args, **kwargs):
        try:
            with codecs.open(self.get_cache_file(), encoding="utf8", mode="w") as fp:
                data = {"zoom": self.zoomlevel}
                if self.window_metrics:
                    data["metrics"] = self.window_metrics
                json.dump(data, fp, indent=2)
        except Exception as e:
            print(f"保存配置失败: {e}")

    def _load_config(self):
        f = Gio.File.new_for_path(self.get_cache_file())
        f.load_contents_async(None, self._finish_loading_history)

    def _finish_loading_history(self, f, res):
        try:
            success, contents, _ = f.load_contents_finish(res)
        except GLib.Error as e:
            print(f"无法加载配置: {e}，使用默认设置")
            contents = "{}"

        try:
            data = json.loads(contents)
        except Exception as e:
            print(f"配置文件无效: {e}，使用默认设置")
            data = {}

        # 恢复缩放级别
        if "zoom" in data:
            try:
                zl = str(data["zoom"])
                idx = self.zoom_ids.index(zl)
                self.zoom.set_active(idx)
                self.zoomlevel = int(zl)
            except (ValueError, IndexError):
                print("恢复缩放级别失败，使用默认值")

        # 恢复窗口位置
        if "metrics" in data:
            try:
                self.restore_window_metrics(data["metrics"])
            except Exception as e:
                print(f"恢复窗口位置失败: {e}")

        self.window_metrics_restored = True
        GLib.idle_add(self.ensure_window_in_screen)


def main():
    setproctitle.setproctitle('magnus')
    m = Main()
    m.app.run(sys.argv)


if __name__ == "__main__":
    main()
